import java.io.File;
import java.io.IOException;
import java.util.Random;
import java.util.Scanner;
import java.util.ArrayList;
import java.util.regex.Pattern;

public class AINET {

	private class Antigen {
		public int AgClass;
		public double[] attributes;
		
		public Antigen(int c) {
			this.AgClass = c;
			this.attributes = new double[numAttributes];
		}
	};
	
	private class Antibody {
		public int AbClass;
		public double[] attributes;
		public double affinity;
		public Antigen Ag;
		
		public Antibody() {
			this.attributes = new double[numAttributes];
		}
	};
	
	// general AINET parameters
	private int numClasses;
	private int numAttributes;
	private double suppressThreshold;
	private double deathThreshold;
	private double AbRatio;
	private double trainingPercent;
	private int maxIter;
	private Random random;
	
	// antigen specific parameters
	private ArrayList<Antigen> AgSet;
	private ArrayList<Antigen> AgTraining;
	private int AgSetSize;
	private int AgTrainingSize;
	private double[] maxValue;
	private double[] minValue;
	
	// antibody specific parameters
	private ArrayList<Antibody> AbSet;
	private ArrayList<Antibody> AbBase;
	private ArrayList<Antibody> AbClonal;
	private int AbSetSize;
	private int AbBaseSize;
	
	// initialize the whole and training antigen sets
	private void initializeAgSets(String AgFile, int[] startIdx) {
		Pattern tokenPattern = Pattern.compile("\\s+"); // whitespace pattern
		int range = 0, trainingCount, idx, maxIdx;
		ArrayList<Integer> selectedIdx;
		
		// initialize the whole antigen set
		AgSet = new ArrayList<AINET.Antigen>(AgSetSize);
		
		// initialize the min and max attribute value arrays
		minValue = new double[numAttributes];
		maxValue = new double[numAttributes];
		
		// read in contents of whole antigen data file
		try {
			// open the antigen data file
			Scanner input = new Scanner(new File(AgFile));
			
			// loop over each antigen to be created
			for (int i = 0, c = 0; i < AgSetSize; i++) {
				// calculate the antigen class
				if ((c < numClasses) && (i >= startIdx[c]))
					c++;
				
				// create antigen
				Antigen Ag = new Antigen(c);
				
				// read in line of antigen data and tokenize
				String line = input.nextLine();
				String[] tokens = tokenPattern.split(line);
				
				// set each attribute of the antigen
				// update the min and max values for each attribute
				for (int a = 0; a < numAttributes; a++) {
					Ag.attributes[a] = Double.parseDouble(tokens[a]);
					
					if (i == 0) {
						minValue[a] = Ag.attributes[a];
						maxValue[a] = Ag.attributes[a];
					}
					else {
						if (Ag.attributes[a] < minValue[a])
							minValue[a] = Ag.attributes[a];
						else if (Ag.attributes[a] > maxValue[a])
							maxValue[a] = Ag.attributes[a];
					}
				}
				
				// add the new antigen to the set
				AgSet.add(Ag);
			}
		}
		catch (IOException ioe) {
			System.err.println(ioe.getMessage());
		}
		
		// initialize the training antigen set
		AgTrainingSize = (int)Math.floor(AgSetSize * trainingPercent);
		AgTraining = new ArrayList<AINET.Antigen>(AgTrainingSize);
		selectedIdx = new ArrayList<Integer>();
		maxIdx = 0;
		
		// loop over each antigen class
		for (int c = 1; c <= numClasses; c++) {
			// calculate the number of antigens in each class
			if (c == numClasses)
				range = AgSetSize - startIdx[c-1];
			else
				range = startIdx[c] - startIdx[c-1];
			
			// calculate how many antigens to select from current
			// class for training
			trainingCount = (int)Math.floor(range * trainingPercent);
			
			// clear index tracking for the class
			selectedIdx.clear();
			
			// randomly select antigens from current class
			// track selected antigens to prevent repeats
			for (int j = 0; j < trainingCount; j++) {
				do {
					idx = startIdx[c-1] + random.nextInt(range);
				} while (selectedIdx.contains(idx));
				selectedIdx.add(idx);
				AgTraining.add(AgSet.get(idx));
				maxIdx++;
			}
		}
		
		// fill in any remaining empty spots in training set
		while (maxIdx < AgTrainingSize) {
			do {
				idx = startIdx[numClasses-1] + random.nextInt(range);
			} while (selectedIdx.contains(idx));
			selectedIdx.add(idx);
			AgTraining.add(AgSet.get(idx));
			maxIdx++;
		}
		
	} // end initializeAgSets method
	
	// initialize the whole, base, and clonal antibody sets
	private void initializeAbSets() {
		int highestIdx;
		double highestAff, aff;
		
		// initialize the whole antibody set
		AbSetSize = (int)Math.floor(AbRatio * AgSetSize);
		AbSet = new ArrayList<AINET.Antibody>(AbSetSize);
		
		// initialize each antibody in the whole set
		for (int i = 0; i < AbSetSize; i++) {
			Antibody Ab = new Antibody();
			
			// initialize each attribute of antibody to randomly fall within
			// min and max values for that attribute
			for (int a = 0; a < numAttributes; a++) {
				Ab.attributes[a] = minValue[a] + random.nextDouble() 
						* (maxValue[a] - minValue[a]);
			}
			
			// calculate which antigen the antibody has highest affinity to
			highestIdx = 0;
			highestAff = 0.0;
			for (int a = 0; a < AgTrainingSize; a++) {
				aff = getAffinity(AgTraining.get(a), Ab);
				if (aff > highestAff) {
					highestAff = aff;
					highestIdx = a;
				}
			}
			
			// set the antibody affinity, class, and antigen based on highest affinity
			Ab.AbClass = AgTraining.get(highestIdx).AgClass;
			Ab.affinity = highestAff;
			Ab.Ag = AgTraining.get(highestIdx);
			
			// add the antibody to the set
			AbSet.add(Ab);
		}
		
		
		// initialize the base antibody set
		AbBaseSize = (int)Math.floor(AbRatio * AgTrainingSize);
		AbBase = new ArrayList<AINET.Antibody>(AbBaseSize);
		
		// select the antibodies with the highest affinities
		// to fill the antibody base set
		for (int i = 0; i < AbBaseSize; i++) {
			highestIdx = 0;
			highestAff = 0.0;
			for (int j = 0; j < AbSetSize; j++) {
				if (AbSet.get(j).affinity > highestAff) {
					highestAff = AbSet.get(j).affinity;
					highestIdx = j;
				}
			}
			
			// add the antibody with the current highest affinity
			// to base set and then set its affinity to 0.0 so not
			// picked again
			AbBase.add(AbSet.get(highestIdx));
			AbSet.get(highestIdx).affinity = 0.0;
		}
		
		// initialize the clonal antibody set
		AbClonal = new ArrayList<AINET.Antibody>();
	}
	
	// calculate the affinity between an antibody and antigen
	// using Manhattan distance
	private double getAffinity(Antigen Ag, Antibody Ab) {
		double dist = 0;
		
		// loop over each attribute
		for (int a = 0; a < numAttributes; a++) {
			// add distance between antigen and antibody to sum
			dist += Math.abs(Ag.attributes[a] - Ab.attributes[a]);
		}
		return (1 / (1 + dist));
	}
	
	// calculate the affinity between two antibodies
	// using Manhattan distance
	private double getAffinity(Antibody Ab1, Antibody Ab2) {
		double dist = 0;
		
		// loop over each attribute
		for (int a = 0; a < numAttributes; a++) {
			// add distance between antigen and antibody to sum
			dist += Math.abs(Ab1.attributes[a] - Ab2.attributes[a]);
		}
		return (1 / (1 + dist));
	}
	
	// calculate the overall affinity of the antibody base set
	// for the test antigen set
	private double getWholeAffinity(ArrayList<Antigen> testSet, int testSetSize) {
		double highestAff, aff;
		int OC = 0, highestIdx;
		
		// loop over the test antigen set
		for (int i = 0; i < testSetSize; i++) {
			// select the antibody with the highest
			// affinity for the antigen
			highestIdx = 0;
			highestAff = 0.0;
			for (int j = 0; j < AbBaseSize; j++) {
				aff = getAffinity(testSet.get(i), AbBase.get(j));
				if (aff > highestAff) {
					highestAff = aff;
					highestIdx = j;
				}
			}
			
			// increment overall correctness if the class of the
			// antibody with highest affinity for the antigen matches
			// the antigen's class
			if (AbBase.get(highestIdx).AbClass == testSet.get(i).AgClass)
				OC++;
		}
		
		return (((double)OC) / testSetSize);
	}
	
	// generate the antibody clonal set from the antibody base set 
	private void clonalExpansion() {
		int cloneCount;
		
		// loop over all antibodies in base set
		for (int i = 0; i < AbBaseSize; i++) {
			// select how many clones to generate based on affinity
			if ((AbBase.get(i).affinity / 2) > random.nextDouble())
				cloneCount = 3;
			else
				cloneCount = random.nextInt(2) + 1; // 1 or 2 clones
			
			// generate a clonal antibody for each clone count
			for (int j = 0; j < cloneCount; j++) {
				Antibody Ab = new Antibody();
				
				// set attributes for the clone
				for (int a = 0; a < numAttributes; a++)
					Ab.attributes[a] = AbBase.get(i).attributes[a];
				
				// set the remaining antibody values
				Ab.AbClass = AbBase.get(i).AbClass;
				Ab.affinity = AbBase.get(i).affinity;
				Ab.Ag = AbBase.get(i).Ag;
				
				// add antibody clone to clonal set
				AbClonal.add(Ab);
			}
		}
	}
	
	// mutate clonal set inversely proportional to affinity
	// recalculate antigen affinity after mutation
	private void affinityMaturation() {
		double alpha, highestAff, aff;
		int highestIdx;
		
		// loop over all antibodies in clonal set
		for (int i = 0; i < AbClonal.size(); i++) {
			// calculate alpha mutation value
			alpha = 1 / AbClonal.get(i).affinity;
			
			// mutate each attribute of the clonal antibody
			for (int a = 0; a < numAttributes; a++) {
				AbClonal.get(i).attributes[a] = AbClonal.get(i).attributes[a] + alpha
					* (AbClonal.get(i).Ag.attributes[a] - AbClonal.get(i).attributes[a]);
			}
			
			// recalculate antigen affinity for clonal antibody
			highestIdx = 0;
			highestAff = 0.0;
			for (int j = 0; j < AgTrainingSize; j++) {
				aff = getAffinity(AgTraining.get(j), AbClonal.get(i));
				if (aff > highestAff) {
					highestAff = aff;
					highestIdx = j;
				}
			}
			
			// set the affinity, class, and antigen based on highest affinity
			AbClonal.get(i).AbClass = AgTraining.get(highestIdx).AgClass;
			AbClonal.get(i).affinity = highestAff;
			AbClonal.get(i).Ag = AgTraining.get(highestIdx);
		}
	}
	
	// remove clonal antibodies whose antigen affinity is less than natural death threshold
	private void metadynamics() {
		// loop backwards over all antibodies in clonal set
		for (int i = (AbClonal.size() - 1); i >= 0; i--) {
			// remove clonal antibody if affinity less than death threshold
			if (AbClonal.get(i).affinity < deathThreshold)
				AbClonal.remove(i);
		}
	}
	
	// remove clonal antibodies whose affinity with each other is less than suppression threshold
	private void clonalSuppression() {
		boolean suppress;
		// loop backwards over all antibodies in clonal set
		for (int i = (AbClonal.size() - 1); i >= 0; i--) {
			// initialize suppression flag
			suppress = false;
			
			// loop backwards over all antibodies earlier in clonal set than current antibody
			for (int j = i - 1; j >= 0; j--) {
				// if current clonal antibody affinity with other antibodies
				// is less than suppression threshold, mark for removal
				if (getAffinity(AbClonal.get(i), AbClonal.get(j)) < suppressThreshold)
					suppress = true;
			}
			
			// if current clonal antibody marked for suppression, remove it
			if (suppress)
				AbClonal.remove(i);
		}
	}
	
	// merge the antibody base set with the clonal set in an empty whole set
	private void networkInteractions() {
		// clear the antibody whole set
		AbSet.clear();
		
		// add all antibodies from base set to whole set
		for (int i = 0; i < AbBaseSize; i++)
			AbSet.add(AbBase.get(i));
		
		// add all antibodies from the clonal set to whole set
		for (int i = 0; i < AbClonal.size(); i++)
			AbSet.add(AbClonal.get(i));
		
		// update the size of the antibody whole set
		AbSetSize = AbSet.size();
	}
	
	// remove those antibodies in new whole set whose affinity with each other
	// is less than suppression threshold
	private void networkSuppression() {
		boolean suppress;
		// loop backwards over all antibodies in whole set
		for (int i = (AbSetSize - 1); i >= 0; i--) {
			// initialize suppression flag
			suppress = false;
			
			// loop backwards over all antibodies earlier in clonal set than current antibody
			for (int j = i - 1; j >= 0; j--) {
				// if current clonal antibody affinity with other antibodies
				// is less than suppression threshold, mark for removal
				if (getAffinity(AbSet.get(i), AbSet.get(j)) < suppressThreshold)
					suppress = true;
			}
			
			// if current clonal antibody marked for suppression, remove it
			if (suppress)
				AbSet.remove(i);
		}
		
		// update the size of the antibody whole set
		AbSetSize = AbSet.size();
	}
	
	// add new random antibodies to whole set for additional diversity
	private void addDiversity() {
		int highestIdx;
		double highestAff, aff;
		
		// create desired number of new random antibodies
		for (int i = 0; i < (10 * AbBaseSize); i++) {
			Antibody Ab = new Antibody();
			
			// initialize each attribute of antibody to randomly fall within
			// min and max values for that attribute
			for (int a = 0; a < numAttributes; a++) {
				Ab.attributes[a] = minValue[a] + random.nextDouble() 
						* (maxValue[a] - minValue[a]);
			}
			
			// calculate antigen affinity for the new antibodies
			highestIdx = 0;
			highestAff = 0.0;
			for (int a = 0; a < AgTrainingSize; a++) {
				aff = getAffinity(AgTraining.get(a), Ab);
				if (aff > highestAff) {
					highestAff = aff;
					highestIdx = a;
				}
			}
			
			// set the antibody affinity, class, and antigen based on highest affinity
			Ab.AbClass = AgTraining.get(highestIdx).AgClass;
			Ab.affinity = highestAff;
			Ab.Ag = AgTraining.get(highestIdx);
			
			// add new antibody to whole set
			AbSet.add(Ab);
		}
		
		// update size of whole antibody set
		AbSetSize = AbSet.size();
	}
	
	// rebuild antibody base set from the antibody whole set
	private void updateAntibodyBase() {
		double highestAff;
		int highestIdx;
		
		// clear the base set
		AbBase.clear();
		
		// select the antibodies with the highest affinities
		// to fill the antibody base set
		for (int i = 0; i < AbBaseSize; i++) {
			highestIdx = 0;
			highestAff = 0.0;
			for (int j = 0; j < AbSetSize; j++) {
				if (AbSet.get(j).affinity > highestAff) {
					highestAff = AbSet.get(j).affinity;
					highestIdx = j;
				}
			}
			
			// add the antibody with the current highest affinity
			// to base set and then set its affinity to 0.0 so not
			// picked again
				AbBase.add(AbSet.get(highestIdx));
				AbSet.get(highestIdx).affinity = 0.0;
		}
	}
	
	// return a string to represent the final results of the aiNET run
	public String printRunResults(int iter) {
		return "Whole Affinity = " + String.format("%1$.3f", getWholeAffinity(AgSet, AgSetSize))
				+ "\nTraining Affinity = " + String.format("%1$.3f", getWholeAffinity(AgTraining, AgTrainingSize))
				+ "\nTotal number of iterations = " + iter + "\n";
	}
	
	// constructor - initialize aiNET parameters, antibody sets, and antigen sets
	public AINET(String AgFile, int c, int a, int s, int[] startIdx, float tp, double r, double dd, double ds) {
		// initialize aiNET parameters
		numClasses = c;
		numAttributes = a;
		AgSetSize = s;
		trainingPercent = tp;
		AbRatio = r;
		deathThreshold = dd;
		suppressThreshold = ds;
		maxIter = 250;
		random = new Random();
		
		// initialize the antigen sets
		initializeAgSets(AgFile, startIdx);
		
		// initialize the antibody sets
		initializeAbSets();
	}
		
	// runs the aiNET algorithm after it has been created and initialized
	public String runAINET() {
		int iter = 0;
		double wholeAff = getWholeAffinity(AgSet, AgSetSize);
		
		// loop over algorithm until max iterations 
		// or minimum whole affinity reached
		while ((wholeAff < 0.99) && (iter < maxIter)) {
			// clonal expansion
			clonalExpansion();
			
			// affinity maturation
			affinityMaturation();
			
			// metadynamics
			metadynamics();
			
			// clonal suppression
			clonalSuppression();
			
			// network interactions
			networkInteractions();
			
			// network suppression
			networkSuppression();
			
			// add diversity
			addDiversity();
			
			// update antibody base
			updateAntibodyBase();
			
			// calculate whole affinity and update iteration
			wholeAff = getWholeAffinity(AgSet, AgSetSize);
			iter++;
		}
		
		// print the results
		return printRunResults(iter);
	}
} // end class AINET
